#include "ui/vsMLModel.h"
#include "vsGlobal.h"

#include <QDragMoveEvent>
#include <QUrl>
#include <QMimeData>

const QString c_mldata_id        = QObject::tr("id");
const QString c_mldata_name      = QObject::tr("Name");
const QString c_mldata_artist    = QObject::tr("Artist");
const QString c_mldata_path      = QObject::tr("Path");
const QString c_mldata_status    = QObject::tr("Status");
const QString c_mldata_size      = QObject::tr("Size");
const QString c_mldata_mime      = QObject::tr("Type");
const QString c_mldata_uprate    = QObject::tr("Up Rate");
const QString c_mldata_dlrate    = QObject::tr("Dl Rate");
const QString c_mldata_seeded    = QObject::tr("Seeded");
const QString c_mldata_requests  = QObject::tr("Requests");
const QString c_mldata_duration  = QObject::tr("Duration");

static QMap<QString, mlData::type> initTbTSMap() // g++ 4.8 32bit doesn't like QMap initialize list
{
    QMap<QString, mlData::type> map;

    map.insert("id",       mlData::id);        map.insert("name",     mlData::name);
    map.insert("artist",   mlData::artist);    map.insert("path",     mlData::path);
    map.insert("status",   mlData::status);    map.insert("size",     mlData::size);
    map.insert("type",     mlData::mime);      map.insert("uprate",   mlData::uprate);
    map.insert("dlrate",   mlData::dlrate);    map.insert("seeded",   mlData::seeded);
    map.insert("requests", mlData::requests);  map.insert("duration", mlData::duration);

    return map;
}

const QMap<QString, mlData::type> mlTypeByTypeString = initTbTSMap(); // g++ 4.8 32bit has an issue with map init list

const QStringList typeStringByMLType = {"id", "name", "artist", "path", "status", "size", "type", "uprate",
                                        "dlrate", "seeded", "requests", "duration" };

const QStringList stringByMLType = { c_mldata_id,   c_mldata_name, c_mldata_artist, c_mldata_path,   c_mldata_status,
                                     c_mldata_size, c_mldata_mime, c_mldata_uprate, c_mldata_dlrate, c_mldata_seeded,
                                     c_mldata_requests, c_mldata_duration };

QString getStringForMLType(mlData::type t)
{
    QString result;

    if (t >= mlData::id && t < mlData::END)
        result = stringByMLType[t];

    return result;
}

QString getTypeStringForMLType(mlData::type t)
{
    QString result;

    if (t >= mlData::id && t < mlData::END)
        result = typeStringByMLType[t];

    return result;
}

mlData::type getMLTypeForTypeString(const QString &idS)
{
    mlData::type t = mlData::END;

    if (mlTypeByTypeString.contains(idS))
        t = mlTypeByTypeString[idS];

    return t;
}


//=====================================================================================


vsMLModel::vsMLModel(QObject *parent) : QAbstractListModel(parent)
{
    //
}

vsMLModel::~vsMLModel()
{
    //
}

QVariant vsMLModel::data(const QModelIndex &index, int role) const
{
    QVariant result;

    if (index.row() < sortedRows.length())
        switch (role)
        {
//        case Qt::DecorationRole:
        case Qt::SizeHintRole: result = QSize(headerTotalWidth, rowHeight); break;
        default:
            break;
        }

    return result;
}

QVariant vsMLModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    QVariant result;

    if (section < headers.size() && orientation == Qt::Horizontal && role == Qt::DisplayRole)
        result = headers[section].str;

    return result;
}

QMap<int, QVariant> vsMLModel::itemData(const QModelIndex &/*index*/) const
{
    vsLog::d("mlModel: requesting item data");
    QMap<int, QVariant> result;
    return result;
}

bool vsMLModel::setData(const QModelIndex &/*index*/, const QVariant &/*value*/, int /*role*/)
{
    vsLog::d("mlModel: set data");
    return false;
}

bool vsMLModel::setItemData(const QModelIndex &/*index*/, const QMap<int, QVariant> &/*roles*/)
{
    vsLog::d("mlModel: set item data");
    return false;
}

bool vsMLModel::setHeaderData(int /*section*/, Qt::Orientation /*orientation*/, const QVariant &/*value*/, int /*role*/)
{
    vsLog::d("mlModel: set header data");
    return false;
}

Qt::ItemFlags vsMLModel::flags(const QModelIndex &/*index*/) const
{
    return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;
}

QStringList vsMLModel::mimeTypes() const { return QStringList() << "text/uri-list" << "application/vnd.text.list"; }

QMimeData* vsMLModel::mimeData(const QModelIndexList &indexes) const
{
    QMimeData *mimeData = new QMimeData();
    QByteArray uriData, idData;
    QDataStream uriWriter(&uriData, QIODevice::WriteOnly),
                idWriter (&idData,  QIODevice::WriteOnly);

    foreach (const QModelIndex &index, indexes)
        if (index.isValid() && index.row() < sortedRows.size())
        {
            QStringList &rowValues = sortedRows[index.row()]->values;
            uriWriter << rowValues[mlData::path];
            idWriter  << rowValues[mlData::id];
        }

    mimeData->setData("text/uri-list",             uriData);
    mimeData->setData("application/vnd.text.list", idData);
    return mimeData;
}

Qt::DropActions vsMLModel::supportedDropActions() const { return Qt::MoveAction; }

bool vsMLModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
                             int row, int /*column*/, const QModelIndex &parent)
{
    bool result = false;

    if (action == Qt::MoveAction && data->hasFormat("application/vnd.text.list"))
    {
        QByteArray encodedData = data->data("application/vnd.text.list");
        QDataStream stream(&encodedData, QIODevice::ReadOnly);
        QStringList ids;
        int first = -1, last = -1;

        if (parent.isValid()) row = parent.row();
        else if (row == -1)   row = sortedRows.length(); // append at end

        while (!stream.atEnd())
        {
            QString id;
            stream >> id;

            if (mappedRows.contains(id))
            {
                ids << id;
                int i = sortedRows.indexOf(mappedRows[id]);

                if (first == -1 || first >  i) first = i;
                if (last  == -1 || last  <= i) last  = i;
            }
        }

        bool foundSequence = !(ids.isEmpty() || first == -1 || last == -1
                               || row == last + 1 || row == first); // shouldn't move before first or after last
        if (foundSequence)
        {
            beginMoveRows(QModelIndex(), first, last, QModelIndex(), row);
            sortedRows.erase(sortedRows.begin() + first, sortedRows.begin() + last + 1);

            if (row > last)
                row -= last - first + 1;

            foreach (const QString &id, ids)
                sortedRows.insert(row, mappedRows[id]);

            endMoveRows();
            redrawEverything();
            result = true;
        }
    }

    return result;
}

void vsMLModel::clearHeaderData()
{
    sortedHeaders.clear();
    mappedHeaders.clear();
    headers.clear();
}

void vsMLModel::addMedia(const vsMLRow &r)
{
    QString id = r.values[mlData::id];

    if (!id.isEmpty() && !hasMedia(id))
    {
        rows.append(r);
        mappedRows[r.values[mlData::id]] = &rows.last();
        sortedRows.append(&rows.last());

        const QModelIndex &i = createIndex(sortedRows.size(), 0);
        emit dataChanged(i, i);
    }
    else
        vsLog::e(tr("Media List Model can't add media with empty or duplicate id")
                 + QString(": %1").arg(id));
}

void vsMLModel::rearrangeHeaders(const QStringList &typeStrings)
{
    if (!typeStrings.isEmpty())
    {
        sortedHeaders.clear();

        foreach (const QString &id, typeStrings)
            if (mlTypeByTypeString.contains(id))
            {
                if (!mappedHeaders.contains(id))
                {
                    mlData::type t = mlTypeByTypeString[id];
                    headers.append(vsMLHeader(t, stringByMLType[t]));
                    mappedHeaders[id] = &headers.last();
                }

                sortedHeaders.append(mappedHeaders[id]);
            }

        redrawEverything();
    }
}

void vsMLModel::resizeHeaders(const QList<int> &widths)
{
    if (!(sortedHeaders.isEmpty() || widths.isEmpty()))
    {
        auto h = sortedHeaders.begin();

        foreach (int newW, widths)
        {
            if (newW <= 0 || newW >= 9000)
                newW = 100;

            (*h)->width = newW;
            h++;

            if (h == sortedHeaders.end())
                break;
        }

        const QModelIndex &first = createIndex(0,                     0),
                          &last  = createIndex(sortedRows.size() - 1, 0);

        emit dataChanged(first, last);
    }
}

void vsMLModel::onMLHeaderResized(int i, int newS, int totalW)
{
    if (sortedHeaders.size() > i)
    {
        headerTotalWidth = totalW;
        sortedHeaders[i]->width = newS;
        redrawEverything();
    }
}

void vsMLModel::onMLHeaderReordered(int i, int newI)
{
    if (sortedHeaders.size() > i)
    {
        sortedHeaders.move(i, newI);
        redrawEverything();
    }
}

QStringList vsMLModel::headerTypeStrings()
{
    QStringList result;

    foreach (vsMLHeader *h, sortedHeaders)
        result << typeStringByMLType[h->type];

    return result;
}

QList<int> vsMLModel::headerSizes()
{
    QList<int> result;

    foreach (vsMLHeader* h, sortedHeaders)
        result << h->width;

    return result;
}

QStringList vsMLModel::mediaIDList() const
{
    QStringList result;

    foreach (const vsMLRow *r, sortedRows)
        result.append(r->values[mlData::id]);

    return result;
}

QStringList vsMLModel::valuesOfRows(const QList<int> &rows, mlData::type t) const
{
    QStringList result;

    if (!rows.isEmpty() && t >= mlData::id && t < mlData::END)
    {
        if (filtering)
        {
            foreach (const int &r, rows)
                if (r < filteredMatch.length() + filteredRest.length())
                {
                    if (r < filteredMatch.length())
                        result << filteredMatch[r]->values[t];
                    else
                        result << filteredRest[r - filteredMatch.length()]->values[t];
                }
        }
        else foreach (const int &r, rows)
            if (r < sortedRows.length())
                result << sortedRows[r]->values[t];
    }

    return result;
}

void vsMLModel::updateMedia(const QString &id, const QMap<mlData::type, QVariant> values)
{
    vsMLRow *row = mappedRows.value(id);

    if (row && !values.isEmpty())
    {
        int rowVI = sortedRows.indexOf(row);

        if (rowVI > -1)
        {
            foreach (mlData::type t, values.keys())
            {
                switch (t)
                {
                case mlData::duration:
                    row->mediaDuration = values[t].toInt();
                    row->values[t] = durationToString(row->mediaDuration);
                    break;
                case mlData::seeded:
                    row->seeded = values[t].toLongLong();
                    row->values[t] = sizeToString(row->seeded);
                    break;
                case mlData::requests:
                    row->requests = values[t].toInt();
                    row->values[t] = values[t].toString();
                    break;
                default:
                    row->values[t] = values[t].toString();
                    break;
                }
            }

            emit dataChanged(createIndex(rowVI, 0), createIndex(rowVI, 0));
        }
    }
}

bool vsMLModel::removeMedia(const QString &id)
{
    bool result = false;
    vsMLRow *row = mappedRows.value(id);

    if (row)
    {
        int rI = sortedRows.indexOf(row);
        beginRemoveRows(QModelIndex(), rI, rI);

        rows.removeAll(*row);
        sortedRows.removeAt(rI);
        mappedRows.remove(id);

        endRemoveRows();
        result = true;
    }

    return result;
}

void vsMLModel::filter(const QString &f)
{
    filtering = !f.isEmpty();

    if (filtering)
    {
        filteredMatch.clear();
        filteredRest.clear();

        QRegExp re(f, Qt::CaseInsensitive);
        bool match = false;

        foreach (vsMLRow *r, sortedRows)
        {
            match = re.indexIn(r->values[mlData::name])   != -1 ||
                    re.indexIn(r->values[mlData::artist]) != -1 ||
                    re.indexIn(r->values[mlData::path])   != -1;

            if (match) filteredMatch.append(r);
            else       filteredRest .append(r);
        }
    }

    redrawEverything();
}

template<typename T> QList<vsMLRow*> typedSort(QList<vsMLRow*> unsorted, Qt::SortOrder order,
                                               mlData::type t, T (*getValue)(vsMLRow*, mlData::type))
{
    typedef QPair<T, QString> TID;

    QList<TID> values;
    QList<vsMLRow*> result;
    QMap<QString, int> sortedIDMap; // key is id (unique), value is index in sortedRows

    for (int i = 0; i < unsorted.length(); ++i)
    {
        vsMLRow *r = unsorted[i];
        const QString &id = r->values[mlData::id];
        values.append(TID(getValue(r, t), id));
        sortedIDMap[id] = i;
    }

    auto lessThan = (order == Qt::AscendingOrder)
        ? [](const TID &s1, const TID &s2) { return s1.first < s2.first; }
        : [](const TID &s1, const TID &s2) { return s2.first < s1.first; };

    qSort(values.begin(), values.end(), lessThan);

    foreach (const TID &tid, values)
        result.append(unsorted[sortedIDMap[tid.second]]);

    return result;
}

void vsMLModel::sort(int column, Qt::SortOrder order)
{
    if (!sortedRows.isEmpty() && column >= 0 && column < headers.size())
    {
        mlData::type t = headers[column].type;

        switch (t)
        {
        case mlData::size:     sortedRows = typedSort<qint64> (sortedRows, order, t,
                [](vsMLRow *r, mlData::type  ){ return r->fileSize; });
            break;
        case mlData::seeded:   sortedRows = typedSort<qint64> (sortedRows, order, t,
                [](vsMLRow *r, mlData::type  ){ return r->seeded; });
            break;
        case mlData::duration: sortedRows = typedSort<int>    (sortedRows, order, t,
                [](vsMLRow *r, mlData::type  ){ return r->mediaDuration; });
            break;
        case mlData::requests: sortedRows = typedSort<int>    (sortedRows, order, t,
                [](vsMLRow *r, mlData::type  ){ return r->requests; });
            break;
        default:               sortedRows = typedSort<QString>(sortedRows, order, t,
                [](vsMLRow *r, mlData::type t){ return r->values[t]; });
            break;
        }

        redrawEverything();
    }
}

bool vsMLModel::changeMediaID(const QString &oldID, const QString &newID)
{
    bool result = false;

    if (mappedRows.contains(oldID))
    {
        vsMLRow *r = mappedRows[oldID];
        int i = 0;

        if (r)
            foreach (vsMLRow *sr, sortedRows)
            {
                if (sr == r) // found it in both map and visual list
                {
                    mappedRows.remove(oldID);
                    mappedRows[newID] = r;
                    r->values[mlData::id] = newID;

                    emit dataChanged(createIndex(i, 0), createIndex(i, 0));
                    result = true;
                    break;
                }

                ++i;
            }
    }

    return result;
}
